package fons.model.ontology.calculation;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.log4j.Logger;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import fons.model.ontology.Context;
import fons.model.ontology.Dimension;
import fons.model.ontology.Filters;
import fons.model.ontology.Match;
import fons.model.ontology.Value;
import fons.navigator.NavigatorMain;
import fons.util.Pair;

public class VariableDefinition {
	private final String name;
	private final Context context;
	private final Map<String, Constraint> constraints = Maps.newHashMap();
	private final Map<String, VariableDefinition> boundVariables = Maps.newHashMap();
	
	public VariableDefinition(String name, Context context) {
		this.name = name;
		this.context = context;
	}
	
	public String getName() {
		return name;
	}
	
	
	public void addConstraint (Constraint constraint) {
		constraint.setVariableDefinition(this);
		this.constraints.put(constraint.getName(), constraint);		
	}
	
	public void addConstraint (BoundConstraint constraint) {
		constraint.getUnboundConstraint().getVariableDefinition().boundVariables.put(getName(), this);
		this.addConstraint((Constraint)constraint);
	}	
	
	
	public boolean hasConstraint (String name) {
		return this.constraints.containsKey(name);
	}
	
	public Constraint getConstraint (String name) {
		return this.constraints.get(name);
	}
		
	synchronized
	public List<Variable> getUnboundVariables() {		
		List<Variable> result = Lists.newArrayList();
		List<String> messages = Lists.newArrayList();
		
		//calculate the Cartesian product to get all the unbound variables
		List<Set<Pair<String, Dimension>>> namedDimensions = Lists.newArrayList();		
		for (Constraint constraint: this.constraints.values()) {
			namedDimensions.add(constraint.getDimensions());
		}
		Set<List<Pair<String, Dimension>>> filtering = Sets.cartesianProduct(namedDimensions);
		
		int i = 0;
		int all = filtering.size();
		
		//filter out the values from the context matching the unbound variables
		//and create all the unbound variables
		Set<Value> allValues = context.getValues();
		nextVariable:
		for (List<Pair<String, Dimension>> eachVariable: filtering) {
			if (i++ % (all/10) == 0) {
				NavigatorMain.getMainFrame().setStatusMessage(NavigatorMain.getMainFrame().getStatusMessage()
					+ (int)((i-1)*100.0/all) + "% "); 
			}
			Match match = new Match();
			Variable variable = new Variable(this.name, this);
			Set<Value> values = allValues;
			
			for (Pair<String, Dimension> namedDimension: eachVariable) {
				match.add(namedDimension.second());
				variable.addConstraint(namedDimension.first(), namedDimension.second());
				
				if (namedDimension.second().getValues().size() < values.size()) {
					values = Sets.newHashSet(namedDimension.second().getValues());
				}
			}
			Set<Value> finalValues = Filters.matching(match).filter(values);
			
			Value value = null;
			if (finalValues.size() > 1) {
				value = finalValues.iterator().next();
				
				for (Value v1: finalValues) {
					for (Value v2: finalValues) {
						if (v1 == v2) continue;
						
						StringBuilder msg = new StringBuilder();
						if (v1.getDifferentDimensions(v2).size() != 0) {
							msg.append("Berechnung ist mehrdeutig: " +
									"Mehrere Werte mit unterschiedlichen Dimensionen " +
									"erf&uuml;llen die Anforderungen. Betrifft: ");
							boolean first = true;
							for (Dimension diff: v1.getDifferentDimensions(v2)) {
								if (!first) msg.append(", ");
								msg.append(diff.getName());
							}
							continue nextVariable;
							
						} else if (v1.getValue() != v2.getValue()) {
							msg.append("Berechnung ist mehrdeutig: " +
									"Verschiedene Werte an denselben Messpunkten gefunden. ");
							
							msg.append("Betrifft: ");
							boolean first = true;
							for (Dimension d: v1.getDimensions()) {
								if (!first) msg.append (" - ");
								msg.append(d.getName());
								first = false;
							};
							messages.add(msg.toString());
							continue nextVariable;
						}
					}
				}
			} else if (finalValues.size() == 1) {
				value = finalValues.iterator().next();
			} 
			
			if (value != null) {
				variable.setValue(finalValues.iterator().next());
				
				//bind the bound variables to the unbound one
				for (VariableDefinition boundVariable: this.boundVariables.values()) {
					final Variable bound = boundVariable.getBoundVariable(variable);
					if (bound != null) {
						variable.bindVariable(bound);
					}
				}
				result.add(variable);
				
			}
		}
		
		for (String msg: messages) {
			Logger.getLogger(VariableDefinition.class).debug(msg);
		}
		Logger.getLogger(VariableDefinition.class).
			warn(messages.size() + " Berechnungen wurden wegen Mehrdeutigkeiten &uuml;bersprungen");
		
		return result;		
	}	
	
	private Variable getBoundVariable(Variable variable) {
		Set<Value> values = null;
		final Match match = new Match();
		final Variable result = new Variable(this.getName(), this);
		
		for (Entry<String, Constraint> constraintEntry: this.constraints.entrySet()) {
			constraintEntry.getValue().setUnboundVariable(variable);
			final Set<Pair<String, Dimension>> otherDimension = constraintEntry.getValue().getDimensions();
			
			//TODO
			assert(otherDimension.size() == 1);
			
			final Dimension dim = otherDimension.iterator().next().second();
			result.addConstraint(constraintEntry.getKey(), dim);
			match.add(dim);
			
			if (values == null || values.size() > dim.getValues().size()) {
				values = Sets.newHashSet(dim.getValues());
			}
		}

		final Set<Value> foundValues = Filters.matching(match).filter(values);
		if (foundValues.size() == 1) {
			result.setValue(foundValues.iterator().next());
			return result;
		}
		return null;
	}

	@Override
	public String toString() {
		return "VariableDefinition [name=" + name + ", constraints="
				+ constraints + " ]";
	}
}